package com.gitee.qdbp.general.common.biz.files.service;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.alibaba.fastjson.JSON;
import com.alibaba.media.MediaConfiguration;
import com.alibaba.media.Result;
import com.alibaba.media.client.MediaClient;
import com.alibaba.media.manage.ManageClient;
import com.alibaba.media.upload.UploadPolicy;
import com.alibaba.media.upload.UploadRequest;
import com.alibaba.media.upload.UploadResponse;
import com.alibaba.media.upload.UploadTokenClient;
import com.gitee.qdbp.able.beans.KeyString;
import com.gitee.qdbp.able.exception.ServiceException;
import com.gitee.qdbp.general.common.api.files.model.FileAuthorization;
import com.gitee.qdbp.general.common.api.files.model.FolderName;
import com.gitee.qdbp.general.common.api.files.model.IPathParams;
import com.gitee.qdbp.general.common.api.files.model.PathParams;
import com.gitee.qdbp.general.common.api.files.service.IWebFileService;
import com.gitee.qdbp.general.common.biz.files.service.wantu.MediaClientFactory;
import com.gitee.qdbp.general.common.biz.files.service.wantu.SimpleClientFactory;
import com.gitee.qdbp.general.common.biz.files.service.wantu.SimpleManageClient;
import com.gitee.qdbp.general.common.biz.files.service.wantu.SimpleMediaClient;
import com.gitee.qdbp.general.common.error.FileErrorCode;
import com.gitee.qdbp.tools.files.FileTools;
import com.gitee.qdbp.tools.files.PathTools;
import com.gitee.qdbp.tools.instance.ExceptionLogger;
import com.gitee.qdbp.tools.utils.ConvertTools;
import com.gitee.qdbp.tools.utils.RandomTools;
import com.gitee.qdbp.tools.utils.VerifyTools;

/**
 * 阿里玩兔云文件服务实现类<br>
 * http://baichuan.taobao.com/docs/doc.htm?treeId=38&articleId=102762&docType=1
 * 
 * <pre>
    wantu.account.ak = 2344xxxx
    wantu.account.sk = 44c871603b83833469189e6bb02cxxxx
    wantu.namespace = xxxx
    # upload.dir: {year} {month} {day} {hour} {minute} {second} {millisecond} {random-1}~{random-10} {ext} {uuid}
    wantu.upload.dir = /{year}/{month}/{day}/
    # upload.name: {year} {month} {day} {hour} {minute} {second} {millisecond} {random-1}~{random-10} {ext} {uuid}
    wantu.upload.name = {hour}{minute}{second}{millisecond}{random-6}.{ext}
    # upload.size.limit: 0 = not limit, 1MB = 1 * 1024 * 1024
    wantu.upload.size.limit = 20 * 1024 * 1024
    # token.expiration: 0 = not limit, 1 hour = 1 * 60 * 60 * 1000
    wantu.token.expiration = 0

    &lt;bean class="com.gitee.qdbp.general.common.biz.files.service.WantuFileService"&gt;
        &lt;constructor-arg name="namespace" value="${wantu.namespace}" /&gt;
        &lt;constructor-arg name="ak" value="${wantu.account.ak}" /&gt;
        &lt;constructor-arg name="sk" value="${wantu.account.sk}" /&gt;
        &lt;property name="folder" value="${wantu.upload.dir}" /&gt;
        &lt;property name="fileName" value="${wantu.upload.name}" /&gt;
        &lt;property name="sizeLimit" value="${wantu.upload.size.limit}" /&gt;
        &lt;property name="expiration" value="${wantu.token.expiration}" /&gt;
    &lt;/bean&gt;
    </pre>
 *
 * @author zhaohuihua
 * @version 160218
 */
//@Service
public class WantuFileService implements IWebFileService {

    /** 日志对象 **/
    private static final Logger log = LoggerFactory.getLogger(WantuFileService.class);

    /** 多媒体客户端 **/
    private MediaClient mediaClient;

    /** 文件大小限制, 0表示无限制 **/
    private Long sizeLimit;
    /** 过期时间(毫秒), 0表示无限制 **/
    private Long expiration;
    /** 上传文件的存储路径(配置项) **/
    private String folder;
    /** 上传文件的存储文件名(配置项) **/
    private String fileName;
    /** 上传文件的存储路径(Java) **/
    private String folderForJava;
    /** 上传文件的存储路径(JS) **/
    private String folderForJs;
    /** 上传文件的存储文件名(Java) **/
    private String fileNameForJava;
    /** 上传文件的存储文件名(JS) **/
    private String fileNameForJs;

    public WantuFileService(String namespace, String ak, String sk) {
        MediaConfiguration config = new MediaConfiguration();
        config.setNamespace(namespace);
        config.setAk(ak);
        config.setSk(sk);

        MediaClientFactory factory = new SimpleClientFactory(config);
        mediaClient = new SimpleMediaClient(factory);

        setFolder("/{year}/{month}/{day}/");
        setFileName("{hour}{minute}{second}{millisecond}{random-6}.{ext}");
    }

    /** {@inheritDoc} **/
    @Override
    public String upload(File file) throws ServiceException {
        UploadRequest req = generateUploadRequest((IPathParams) null);
        req.setFile(file);
        return upload(req);
    }

    /** {@inheritDoc} **/
    @Override
    public String upload(String extension, byte[] bytes) throws ServiceException {
        UploadRequest req = generateUploadRequest((IPathParams) null);
        req.setInputStream(new ByteArrayInputStream(bytes), bytes.length);
        return upload(req);
    }

    /** {@inheritDoc} **/
    @Override
    public String upload(String extension, InputStream input, long size) throws ServiceException {
        UploadRequest req = generateUploadRequest((IPathParams) null);
        req.setInputStream(input, size);
        return upload(req);
    }

    /** {@inheritDoc} **/
    @Override
    public String upload(IPathParams params, File file) throws ServiceException {
        UploadRequest req = generateUploadRequest(params);
        req.setFile(file);
        return upload(req);
    }

    /** {@inheritDoc} **/
    @Override
    public String upload(IPathParams params, String extension, byte[] bytes) throws ServiceException {
        UploadRequest req = generateUploadRequest(params);
        req.setInputStream(new ByteArrayInputStream(bytes), bytes.length);
        return upload(req);
    }

    /** {@inheritDoc} **/
    @Override
    public String upload(IPathParams params, String extension, InputStream input, long size) throws ServiceException {
        UploadRequest req = generateUploadRequest(params);
        req.setInputStream(input, size);
        return upload(req);
    }

    /** {@inheritDoc} **/
    @Override
    public String upload(IPathParams params, String url) throws ServiceException {
        if (isInsideFile(url)) {
            if (isTempsFile(url)) {
                return moveTo(url, params);
            } else {
                return url;
            }
        } else {
            // 先通过URL下载到临时目录, 再上传到文件服务器
            String extension = PathTools.getExtension(url);
            String folder = System.getProperty("java.io.tmpdir");
            String filename = PathTools.replaceExtension(RandomTools.generateUuid(), extension);
            String saveAsPath = PathTools.concat(folder, filename);
            try { // 下载到临时目录
                PathTools.downloadSave(url, saveAsPath);
            } catch (IOException e) {
                throw new ServiceException(FileErrorCode.FILE_UPLOAD_ERROR, e);
            }
            File source = new File(saveAsPath);
            String newurl = upload(params, source);
            // 删除临时文件
            FileTools.delete(source, ExceptionLogger.SIMPLE);
            return newurl;
        }
    }

    // http://zhh.image.alimmdn.com/files/2016/05/09/1404216722139535.png
    private static final Pattern INSIDE = Pattern.compile("^https?://([^\\.]+\\.)+alimmdn.com/");

    /** {@inheritDoc} **/
    @Override
    public boolean isInsideFile(String url) throws ServiceException {
        return INSIDE.matcher(url).find();
    }

    /** {@inheritDoc} **/
    @Override
    public boolean isTempsFile(String url) throws ServiceException {
        VerifyTools.requireNotBlank(url, "url");
        Matcher matcher = INSIDE.matcher(url);
        if (!matcher.find()) {
            return false;
        }
        String relativePath = matcher.replaceFirst("");
        if (relativePath.startsWith("/")) {
            relativePath = relativePath.substring(1);
        }
        return relativePath.startsWith(FolderName.temps.name() + "/");
    }

    /** {@inheritDoc} **/
    @Override
    public String moveTo(String url, IPathParams params) throws ServiceException {

        VerifyTools.requireNotBlank(url, "url");
        VerifyTools.requireNotBlank(params, "params");

        Matcher matcher = INSIDE.matcher(url);
        if (!matcher.find()) {
            return url;
        }
        // 原路径相对路径
        String baseUrl = matcher.group(0);
        String srcRelativePath = matcher.replaceFirst("");
        if (srcRelativePath.startsWith("/")) {
            srcRelativePath = srcRelativePath.substring(1);
        }

        // 根据源路径和带占位符的路径配置生成移动后的目标路径
        String placeholderPath = PathTools.concat(this.folder, this.fileName);
        String destRelativePath = FileHandleTools.generateMoveTargetPath(srcRelativePath, placeholderPath, params);

        if (srcRelativePath.equals(destRelativePath)) {
            return url; // 源文件与目标文件相同, 无需移动
        }
        // 移动至目标路径
        ManageClient manageClient = ((SimpleMediaClient) mediaClient).getManageClient();
        Result<Void> result = ((SimpleManageClient) manageClient).rename(srcRelativePath, destRelativePath);
        if (result.getHttpStatus() == 200 && result.isSuccess()) {
            // 调用接口成功,打印出上传接口的返回信息
            log.trace("File move success, {} --> {}, {}", srcRelativePath, destRelativePath, result.toString());
            // 返回移动后的完整路径
            return PathTools.concat(baseUrl, destRelativePath);
        } else {
            // 调用接口失败,输出错误信息
            log.trace("File move failed, {} --> {}, {}", srcRelativePath, destRelativePath, result.toString());
            if (result.getHttpStatus() == 404) {
                throw new ServiceException(FileErrorCode.FILE_NOT_FOUND);
            } else if ("NameDuplicated".equals(result.getCode())) {
                throw new ServiceException(FileErrorCode.FILE_ALREADY_EXISTS);
            } else if ("LimitExceeded".equals(result.getCode())) {
                throw new ServiceException(FileErrorCode.FILE_SIZE_EXCEEDED);
            } else {
                throw new ServiceException(FileErrorCode.FILE_MOVE_ERROR);
            }
        }
    }

    /** {@inheritDoc} **/
    @Override
    public void deleteFile(String url) throws ServiceException {
        VerifyTools.requireNotBlank(url, "url");

        Matcher matcher = INSIDE.matcher(url);
        if (!matcher.find()) {
            return; // 不是站内路径
        }
        // 相对路径
        String relativePath = matcher.replaceFirst("");
        if (relativePath.startsWith("/")) {
            relativePath = relativePath.substring(1);
        }

        String dir;
        String name;
        { // 拆分文件夹和文件名
            int index = relativePath.lastIndexOf('/');
            if (index < 0) {
                dir = "/";
                name = relativePath;
            } else if (index == 0) {
                dir = "/";
                name = relativePath.substring(index + 1);
            } else {
                dir = relativePath.substring(0, index);
                name = relativePath.substring(index + 1);
            }
        }
        if (!dir.startsWith("/")) {
            dir = "/" + dir;
        }
        Result<Void> result = mediaClient.deleteFile(dir, name);
        if (result.getHttpStatus() == 200 && result.isSuccess()) {
            // 调用接口成功,打印出上传接口的返回信息
            log.trace("File delete success, {}, {}", relativePath, result.toString());
        } else {
            // 调用接口失败,输出错误信息
            log.trace("File delete failed, {}, {}", relativePath, result.toString());
            if (result.getHttpStatus() == 404) {
                throw new ServiceException(FileErrorCode.FILE_NOT_FOUND);
            } else {
                throw new ServiceException(FileErrorCode.FILE_DELETE_ERROR);
            }
        }
    }

    /** {@inheritDoc} **/
    @Override
    public FileAuthorization authorize() throws ServiceException {
        return authorize(null);
    }

    /** {@inheritDoc} **/
    @Override
    public FileAuthorization authorize(IPathParams params) throws ServiceException {
        // 鉴权客户端
        UploadTokenClient tokenClient = ((SimpleMediaClient) mediaClient).getUploadTokenClient();
        // 生成上传策略
        UploadPolicy policy = generateUploadPolicy(params, false);
        String key = tokenClient.getUploadToken(policy);

        FileAuthorization authorization = new FileAuthorization();
        authorization.setToken(key);
        authorization.setSizeLimit(sizeLimit);
        return authorization;
    }

    /** 设置文件大小限制, 0表示无限制 **/
    public void setSizeLimit(String sizeLimit) {
        if (VerifyTools.isNotBlank(sizeLimit)) {
            Long number = ConvertTools.toLong(sizeLimit);
            if (number != null && number > 0) {
                this.sizeLimit = number;
            }
        }
    }

    /** 获取文件大小限制, 0表示无限制 **/
    public Long getSizeLimit() {
        return sizeLimit;
    }

    /** 设置过期时间(毫秒), 0表示无限制 **/
    public void setExpiration(String expiration) {
        if (VerifyTools.isNotBlank(expiration)) {
            Long number = ConvertTools.toLong(expiration);
            if (number != null && number > 0) {
                this.expiration = number;
            }
        }
    }

    /** 获取过期时间(毫秒), 0表示无限制 **/
    public Long getExpiration() {
        return expiration;
    }

    /** 上传文件的存储路径 **/
    public void setFolder(String folder) {
        if (VerifyTools.isNotBlank(folder)) {
            this.folder = folder;
            this.folderForJava = formatPlaceholder(folder, true);
            this.folderForJs = formatPlaceholder(folder, false);
        }
    }

    /** 获取上传文件的存储路径 **/
    public String getFolderForJava() {
        return folderForJava;
    }

    /** 获取上传文件的存储路径 **/
    public String getFolderForJs() {
        return folderForJs;
    }

    /** 上传文件的存储文件名 **/
    public void setFileName(String fileName) {
        if (VerifyTools.isNotBlank(fileName)) {
            this.fileName = fileName;
            this.fileNameForJava = formatPlaceholder(fileName, true);
            this.fileNameForJs = formatPlaceholder(fileName, false);
        }
    }

    /** 获取上传文件的存储文件名 **/
    public String getFileNameForJava() {
        return fileNameForJava;
    }

    /** 获取上传文件的存储文件名 **/
    public String getFileNameForJs() {
        return fileNameForJs;
    }

    protected String upload(UploadRequest request) throws ServiceException {
        Result<UploadResponse> result = mediaClient.upload(request);
        if (result.getHttpStatus() == 200 && result.isSuccess()) {
            // 调用接口成功,打印出上传接口的返回信息
            log.trace("File upload success, " + JSON.toJSONString(result.getData()));
            return result.getData().getUrl();
        } else {
            // 调用接口失败,输出错误信息
            log.trace("File upload failed, " + JSON.toJSONString(result));
            throw new ServiceException(FileErrorCode.FILE_UPLOAD_ERROR);
        }
    }

    /**
     * 占位符处理, 由于spring会处理${ext}类似的占位符, 造成报错<br>
     * 只好配置成{ext}, 然后替换成${ext}<br>
     * <br>
     * 由于系统占位符没有毫秒和随机数, <br>
     * 而且年月日时分秒也不是固定位数(/img/${year}${month}${day}, 1月25日和12月5日都是/img/2016125)<br>
     * 就准备用自定义占位符var-*来实现<br>
     * 结果发现上传策略UploadPolicy在Java SDK和JS SDK表现还不一致<br>
     * Java<br>
     * vars.put("var-x-year", "2016");<br>
     * vars.put("var-x-month", "05");<br>
     * vars.put("var-x-day", "11");<br>
     * UploadRequest req = new UploadRequest(policy);<br>
     * req.setVars(vars);<br>
     * JS<br>
     * formData.append("var-x-year", "2016");<br>
     * formData.append("var-x-month", "05");<br>
     * formData.append("var-x-day", "11");<br>
     * <br>
     * Java dir = /images/${var-x-year}${var-x-month}${var-x-day}<br>
     * JS dir = /images/${x-year}${x-month}${x-day}<br>
     * <br>
     * Java的UploadPolicy必须加上ver-才行, 而JS不能加!<br>
     * 所有调用upload(*)方法上传的都是Java模式; 而调用authorize()方法才是JS模式, 获取授权后通过JS直接上传到阿里云服务器<br>
     *
     * @param params
     * @return
     */
    protected UploadPolicy generateUploadPolicy(IPathParams params, boolean java) {
        // 生成上传策略
        String dir;
        if (params != null) {
            dir = PathTools.concat(true, params.name(), java ? folderForJava : folderForJs);
        } else {
            dir = PathTools.concat(true, FolderName.files.name(), java ? folderForJava : folderForJs);
        }

        UploadPolicy policy = new UploadPolicy();
        policy.setInsertOnly(UploadPolicy.INSERT_ONLY);
        policy.setDir(dir);
        policy.setName(java ? fileNameForJava : fileNameForJs);

        if (sizeLimit != null && sizeLimit > 0) {
            policy.setSizeLimit(sizeLimit);
        }
        if (expiration != null && expiration > 0) {
            policy.setExpiration(System.currentTimeMillis() + expiration);
        } else {
            policy.setExpiration(-1);
        }

        return policy;
    }

    protected UploadRequest generateUploadRequest(IPathParams params) {
        // 生成上传策略
        UploadPolicy policy = generateUploadPolicy(params, true);

        Date date = new Date();

        // 生成参数, 作为自定义占位符
        Map<String, String> vars = new HashMap<>();
        // 日期时间
        for (KeyString i : TIME_PARTS) {
            vars.put("var-x-" + i.getKey(), new SimpleDateFormat(i.getValue()).format(date));
        }
        // 随机数1~10位
        String number = RandomTools.generateNumber(RANDOM_SIZE);
        for (int i = 1; i < RANDOM_SIZE; i++) {
            vars.put("var-x-random-" + i, number.substring(0, i));
        }
        vars.put("var-x-random-" + RANDOM_SIZE, number);
        if (params instanceof PathParams) {
            PathParams options = (PathParams) params;
            Map<String, Object> extra = options.getExtra();
            if (extra != null && !extra.isEmpty()) {
                for (Entry<String, Object> entry : extra.entrySet()) {
                    vars.put("var-x-" + entry.getKey(), String.valueOf(entry.getValue()));
                }
            }
        }

        UploadRequest req = new UploadRequest(policy);
        req.setVars(vars);

        return req;
    }

    private static Pattern PLACEHOLDER = Pattern.compile("\\{([\\w+\\.\\-]+)\\}");

    // path:  /{year}/{month}/{day}/{random-10}.{ext}
    // js  :  /${x-year}/${x-month}/${x-day}/${x-random-10}.${ext}
    // java:  /${var-x-year}/${var-x-month}/${var-x-day}/${var-x-random-10}.${ext}
    private static String formatPlaceholder(String path, boolean java) {
        StringBuilder buffer = new StringBuilder();
        String v = java ? "var-x-" : "x-";
        Matcher matcher = PLACEHOLDER.matcher(path);
        int i = 0;
        // 按照js/java要求的格式, 生成占位符参数
        while (matcher.find()) {
            buffer.append(path.substring(i, matcher.start()));
            String word = matcher.group(1);
            if (SYSTEM_PLACEHOLDER.containsKey(word)) {
                buffer.append(SYSTEM_PLACEHOLDER.get(word));
            } else {
                buffer.append("${").append(v).append(matcher.group(1)).append("}");
            }
            i = matcher.end();
        }
        buffer.append(path.substring(i));
        return buffer.toString();
    }

    private static int RANDOM_SIZE = 10;
    private static List<KeyString> TIME_PARTS = new ArrayList<>();
    private static Map<String, String> SYSTEM_PLACEHOLDER = new HashMap<>();

    static {
        // 初始化时间占位符的格式化参数
        TIME_PARTS.add(new KeyString("year", "yyyy"));
        TIME_PARTS.add(new KeyString("month", "MM"));
        TIME_PARTS.add(new KeyString("day", "dd"));
        TIME_PARTS.add(new KeyString("hour", "HH"));
        TIME_PARTS.add(new KeyString("minute", "mm"));
        TIME_PARTS.add(new KeyString("second", "ss"));
        TIME_PARTS.add(new KeyString("millisecond", "SSS"));

        // 初始化占位符的替换参数
        // 由于${xxx}是spring占位符, 因此文件路径占位符改为{xxx}, 在使用时再替换为${xxx}
        SYSTEM_PLACEHOLDER.put("namespace", "${namespace}");
        SYSTEM_PLACEHOLDER.put("mediaType", "${mediaType}");
        SYSTEM_PLACEHOLDER.put("fileSize", "${fileSize}");
        SYSTEM_PLACEHOLDER.put("uuid", "${uuid}");
        SYSTEM_PLACEHOLDER.put("width", "${width}");
        SYSTEM_PLACEHOLDER.put("height", "${height}");
        SYSTEM_PLACEHOLDER.put("ext", "${ext}");
    }

}
